引言
关于什么是OC Runtime这里不再赘述,可以参考Objective-C Runtime简述这篇文章。
这里讨论Objective-C Runtime在实际开发中是如何使用的,并举栗子加以说明。要使用runtime,要先引入头文件#import <objc/runtime.h>
。
实践
1.动态的添加成员变量和方法,修改成员变量值
//1.修改实例变量值
void changeInstanceVariableValue() {
Person *person = [Person new];
person.name = @"Tom";
NSLog(@"name==%@", person.name);
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([person class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *varName = ivar_getName(ivar);
NSString *name = [NSString stringWithUTF8String:varName];
if ([name isEqualToString:@"_name"]) {
object_setIvar(person, ivar, @"Jerry");
break;
}
}
NSLog(@"修改后 name==%@", person.name);
}
//c形式的函数,必须有两个指定参数(id self,SEL _cmd),参考oc runtime 消息(message)
void run(id self, SEL _cmd) {
NSLog(@"%@ is running!", self);
}
//2.添加方法
//"v@:" 表示函数类型,v代表无返回值void,如果是i则代表int;@代表 id sel; : 代表 SEL _cmd;
void addMethod() {
class_addMethod([Person class], @selector(run), (IMP)run, "v@:");
Person *person = [Person new];
person.name = @"Tom";
// [p run];
[person performSelector:@selector(run)];
}
2.交换方法的实现,拦截并替换方法
OC Method Siwzzling的实现原理就是方法交换。利用方法交换,可以替换系统方法,在系统方法上增加额外的功能,比如打印一些必要的参数。
//3.交换方法实现
void exchangeMethodImplementation() {
Method eatMethod = class_getInstanceMethod([Person class], @selector(eat));
Method sleepMethod = class_getInstanceMethod([Person class], @selector(sleep));
method_exchangeImplementations(eatMethod, sleepMethod);
Person *person = [Person new];
person.name = @"Tom";
[person eat];
[person sleep];
}
3.实现分类也可以增加属性
利用Associated Object可以实现分类添加属性。可以参考之前的博客:Objective-C Runtime之Associated Objects
4.实现NSCoding的自动归档和自动解档
自定义模型要实现归档和解档需要实现NSCoding
协议的- (instancetype)initWithCoder:(NSCoder *)aDecoder
和- (void)encodeWithCoder:(NSCoder *)aCoder
方法。
比如:Person类有名字、年龄、身高和体重4个属性:
@interface Person : NSObject
@property (copy, nonatomic) NSString *name; ///< 名字
@property (assign, nonatomic) NSInteger age; ///< 年龄
@property (assign, nonatomic) float height; ///< 身高
@property (assign, nonatomic) float width; ///< 体重
@end
通常归档和解档时的实现方式是这样的:
//解档
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_name = [aDecoder decodeObjectForKey:@"name"];
_age = [aDecoder decodeIntegerForKey:@"age"];
_height = [aDecoder decodeFloatForKey:@"height"];
_width = [aDecoder decodeFloatForKey:@"width"];
}
return self;
}
//归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_name forKey:@"name"];
[aCoder encodeInteger:_age forKey:@"age"];
[aCoder encodeFloat:_height forKey:@"height"];
[aCoder encodeFloat:_width forKey:@"width"];
}
一旦模型类的属性多时,这种重复劳动显得毫无意义。既然模型类继承自NSOject类,我们可以给NSObject增加实现自动归档和解档功能的分类
,从而一劳永逸。
//NSObject+AutoArchive.h
#import <Foundation/Foundation.h>
//将归解档两个方法定义为宏,方便调用
#define NSOBJECT_AUTOARCHIVE_METHOD \
- (instancetype)initWithCoder:(NSCoder *)aDecoder {\
if (self = [super init]) {\
[self decode:aDecoder];\
}\
return self;\
}\
\
- (void)encodeWithCoder:(NSCoder *)aCoder {\
[self encode:aCoder];\
}
@interface NSObject (AutoArchive)
//不需要归解档的属性数组
- (NSArray *)ignoredPropertyNames;
//Archiver
- (void)encode:(NSCoder *)aCoder;
//Unarchiver
- (void)decode:(NSCoder *)aDecoder;
@end
//NSObject+AutoArchive.m
#import "NSObject+AutoArchive.h"
#import <objc/runtime.h>
@implementation NSObject (AutoArchive)
- (NSArray *)ignoredPropertyNames {
return nil;
}
- (void)encode:(NSCoder *)aCoder {
// 一层层父类往上查找,对父类的属性执行归档方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
if ([self respondsToSelector:@selector(ignoredPropertyNames)]) {
if ([[self ignoredPropertyNames] containsObject:ivarName]) {
continue;
}
}
// ivarName = [ivarName substringFromIndex:1];
id value = [self valueForKey:ivarName];
[aCoder encodeObject:value forKey:ivarName];
}
free(ivars);
c = [c superclass];
}
}
- (void)decode:(NSCoder *)aDecoder {
// 一层层父类往上查找,对父类的属性执行解档方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
if ([self respondsToSelector:@selector(ignoredPropertyNames)]) {
if ([[self ignoredPropertyNames] containsObject:ivarName]) {
continue;
}
}
// ivarName = [ivarName substringFromIndex:1];
id value = [aDecoder decodeObjectForKey:ivarName];
[self setValue:value forKey:ivarName];
}
free(ivars);
c = [c superclass];
}
}
@end
于是乎,Person类的归档和解档就可以这样写:
#import "Person.h"
#import "NSObject+AutoArchive.h"
@interface Person ()<NSCoding>
@end
@implementation Person
NSOBJECT_AUTOARCHIVE_METHOD
@end
是不是变得很简洁了啊,如果有不需要归解档的属性就实现ignoredPropertyNames方法。源码可以参考这里。
5.实现字典和模型的自动转换
字典转模型的应用可以说是每个app必然会使用的场景,实现原理大概是:利用runtime遍历模型中所有属性,根据属性名,去字典中查找key,取出对应的值,给模型的属性赋值(ps:也可以使用KVC的setValuesForKeysWithDictionary:
方法来字典转模型,但对于属性是其他模型或者模型数组的情况,我们需要重写setValue:forUndefinedKey:
方法来判断key,防止crash,这样是不是还复杂一些、效率更低呢)。
字典转模型我们需要考虑三种特殊情况:
- 当字典的key和模型的属性匹配不上
- 模型中嵌套模型(模型属性是另外一个模型对象)
- 数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)
对于第一种情况,若模型属性个数小于字典键值数,这时候不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对不会去遍历;若模型属性个数大于字典键值数,在字典中取不到值,利用kvc赋值nil时会抛出异常,所以要判断一下,取值为nil时,直接跳过。代码如下:
- (void)setDict:(NSDictionary *)dict {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
// 成员变量名转为属性名(去掉下划线 _ )
ivarName = [ivarName substringFromIndex:1];
// 取出字典的值
id value = [dict objectForKey:ivarName];
if (!value) {//取值为nil时,直接跳过
continue;
}//kvc赋值
[self setValue:value forKey:ivarName];
}
free(ivars);
}
对于第二种情况,我们要取得嵌套模型的类型,然后递归生成模型对象再赋值给外层模型的对应模型属性,runtime的ivar_getTypeEncoding 方法获取模型对象类型,比如NSString *name
–对应–>@"NSString"
, Person *p
–对应–>@"Person"
, NSInteger age
–对应–>i
(可参看runtime type encoding)。注意的是,这里要剔除Foundation框架的以NS为前缀的类。代码如下:
- (void)setDict:(NSDictionary *)dict {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
// 成员变量名转为属性名(去掉下划线 _ )
ivarName = [ivarName substringFromIndex:1];
// 取出字典的值
id value = [dict objectForKey:ivarName];
if (!value) {//取值为nil时,直接跳过
continue;
}
// 获得成员变量的类型
NSString *ivarType = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
if ([ivarType hasPrefix:@"@"]) { //是对象
ivarType = [ivarType substringWithRange:NSMakeRange(2, ivarType.length-3)]; //@"Person"----->Person
if (![ivarType hasPrefix:@"NS"]) {//去除NS类
Class class = NSClassFromString(ivarType);
value = [class objectWithDict:value];
}
}
//kvc赋值
[self setValue:value forKey:ivarName];
}
free(ivars);
}
第三种情况是模型的属性是一个数组,数组中是一个个模型对象,对象类型未知,调用objectClassName
方法获取对象类型,然后递归创建对象类型,加入数组中,最后赋值给外层模型的数组属性,代码如下:
- (void)setDict:(NSDictionary *)dict {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
// 成员变量名转为属性名(去掉下划线 _ )
ivarName = [ivarName substringFromIndex:1];
// 取出字典的值
id value = [dict objectForKey:ivarName];
if (!value) {//取值为nil时,直接跳过
continue;
}
// 获得成员变量的类型
NSString *ivarType = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
if ([ivarType hasPrefix:@"@"]) { //是对象
ivarType = [ivarType substringWithRange:NSMakeRange(2, ivarType.length-3)]; //@"Person"----->Person
if (![ivarType hasPrefix:@"NS"]) {//去除NS类
Class class = NSClassFromString(ivarType);
value = [class objectWithDict:value];
} else if ([ivarType isEqualToString:@"NSArray"]) {//数组
if ([self respondsToSelector:@selector(objectClassName)]) {
NSString *objectClassName = [self objectClassName];
if (objectClassName) {
Class class = NSClassFromString(objectClassName);
NSMutableArray *arr = [NSMutableArray array];
for (id item in value) {
[arr addObject:[class objectWithDict:item]];
}
value = arr;
}
}
}
}
//kvc赋值
[self setValue:value forKey:ivarName];
}
free(ivars);
}
就像归档一样,可以增加Class c = self.class;
while (c &&c != [NSObject class]) {...c = [c superclass]};
来从子类到父类进行字典转换。
参考
https://github.com/Tuccuay/RuntimeSummary
JSONModel
MJExtension
YYModel
https://segmentfault.com/a/1190000003882034
http://www.jianshu.com/p/ab966e8a82e2